Jelajahi discriminated unions di TypeScript, sebuah alat yang kuat untuk membangun state machine yang kokoh dan type-safe. Pelajari cara mendefinisikan state, menangani transisi, dan memanfaatkan sistem tipe TypeScript untuk meningkatkan keandalan kode.
TypeScript Discriminated Unions: Membangun State Machine yang Type-Safe
Dalam dunia pengembangan perangkat lunak, mengelola state aplikasi secara efektif sangatlah penting. State machine menyediakan abstraksi yang kuat untuk memodelkan sistem stateful yang kompleks, memastikan perilaku yang dapat diprediksi dan menyederhanakan pemahaman tentang logika sistem. TypeScript, dengan sistem tipenya yang kokoh, menawarkan mekanisme fantastis untuk membangun state machine yang type-safe menggunakan discriminated unions (juga dikenal sebagai tagged unions atau algebraic data types).
Apa itu Discriminated Unions?
Discriminated union adalah sebuah tipe yang merepresentasikan sebuah nilai yang bisa menjadi salah satu dari beberapa tipe yang berbeda. Setiap tipe ini, yang dikenal sebagai anggota dari union, memiliki properti umum yang khas yang disebut diskriminan atau tag. Diskriminan ini memungkinkan TypeScript untuk menentukan dengan tepat anggota mana dari union yang sedang aktif, memungkinkan pengecekan tipe dan pelengkapan otomatis (auto-completion) yang kuat.
Bayangkan seperti lampu lalu lintas. Lampu tersebut bisa berada dalam salah satu dari tiga keadaan: Merah, Kuning, atau Hijau. Properti 'warna' bertindak sebagai diskriminan, yang memberitahu kita secara pasti keadaan lampu saat ini.
Mengapa Menggunakan Discriminated Unions untuk State Machine?
Discriminated unions memberikan beberapa manfaat utama saat membangun state machine di TypeScript:
- Keamanan Tipe (Type Safety): Compiler dapat memverifikasi bahwa semua state dan transisi yang mungkin telah ditangani dengan benar, mencegah galat saat runtime (runtime error) yang terkait dengan transisi state yang tidak terduga. Ini sangat berguna dalam aplikasi yang besar dan kompleks.
- Pengecekan Kelengkapan (Exhaustiveness Checking): TypeScript dapat memastikan bahwa kode Anda menangani semua state yang mungkin dari state machine, memberitahu Anda pada saat kompilasi jika ada state yang terlewat dalam pernyataan kondisional atau switch case. Ini membantu mencegah perilaku yang tidak terduga dan membuat kode Anda lebih kokoh.
- Keterbacaan yang Lebih Baik: Discriminated unions secara jelas mendefinisikan state yang mungkin dari sistem, membuat kode lebih mudah dipahami dan dipelihara. Representasi state yang eksplisit meningkatkan kejelasan kode.
- Pelengkapan Kode yang Ditingkatkan: Intellisense dari TypeScript menyediakan saran pelengkapan kode yang cerdas berdasarkan state saat ini, mengurangi kemungkinan kesalahan dan mempercepat pengembangan.
Mendefinisikan State Machine dengan Discriminated Unions
Mari kita ilustrasikan cara mendefinisikan state machine menggunakan discriminated unions dengan contoh praktis: sistem pemrosesan pesanan. Sebuah pesanan dapat berada dalam state berikut: Pending, Processing, Shipped, dan Delivered.
Langkah 1: Definisikan Tipe State
Pertama, kita mendefinisikan tipe individual untuk setiap state. Setiap tipe akan memiliki properti `type` yang bertindak sebagai diskriminan, beserta data spesifik untuk state tersebut.
interface Pending {
type: "pending";
orderId: string;
customerName: string;
items: string[];
}
interface Processing {
type: "processing";
orderId: string;
assignedAgent: string;
}
interface Shipped {
type: "shipped";
orderId: string;
trackingNumber: string;
}
interface Delivered {
type: "delivered";
orderId: string;
deliveryDate: Date;
}
Langkah 2: Buat Tipe Discriminated Union
Selanjutnya, kita membuat discriminated union dengan menggabungkan tipe-tipe individual ini menggunakan operator `|` (union).
type OrderState = Pending | Processing | Shipped | Delivered;
Sekarang, `OrderState` merepresentasikan sebuah nilai yang bisa berupa `Pending`, `Processing`, `Shipped`, atau `Delivered`. Properti `type` di dalam setiap state bertindak sebagai diskriminan, memungkinkan TypeScript untuk membedakannya.
Menangani Transisi State
Setelah kita mendefinisikan state machine kita, kita memerlukan mekanisme untuk beralih antar state. Mari kita buat fungsi `processOrder` yang menerima state saat ini dan sebuah action sebagai input, lalu mengembalikan state yang baru.
interface Action {
type: string;
payload?: any;
}
function processOrder(state: OrderState, action: Action): OrderState {
switch (state.type) {
case "pending":
if (action.type === "startProcessing") {
return {
type: "processing",
orderId: state.orderId,
assignedAgent: action.payload.agentId,
};
}
return state; // Tidak ada perubahan state
case "processing":
if (action.type === "shipOrder") {
return {
type: "shipped",
orderId: state.orderId,
trackingNumber: action.payload.trackingNumber,
};
}
return state; // Tidak ada perubahan state
case "shipped":
if (action.type === "deliverOrder") {
return {
type: "delivered",
orderId: state.orderId,
deliveryDate: new Date(),
};
}
return state; // Tidak ada perubahan state
case "delivered":
// Pesanan sudah terkirim, tidak ada tindakan lebih lanjut
return state;
default:
// Ini seharusnya tidak pernah terjadi karena pengecekan kelengkapan
return state; // Atau lemparkan galat
}
}
Penjelasan
- Fungsi `processOrder` menerima `OrderState` saat ini dan sebuah `Action` sebagai input.
- Fungsi ini menggunakan pernyataan `switch` untuk menentukan state saat ini berdasarkan diskriminan `state.type`.
- Di dalam setiap `case`, fungsi ini memeriksa `action.type` untuk menentukan apakah transisi yang valid dipicu.
- Jika transisi yang valid ditemukan, fungsi ini mengembalikan objek state baru dengan `type` dan data yang sesuai.
- Jika tidak ada transisi yang valid ditemukan, fungsi ini mengembalikan state saat ini (atau melemparkan galat, tergantung pada perilaku yang diinginkan).
- Kasus `default` disertakan untuk kelengkapan dan idealnya tidak akan pernah tercapai berkat pengecekan kelengkapan (exhaustiveness checking) dari TypeScript.
Memanfaatkan Pengecekan Kelengkapan (Exhaustiveness Checking)
Pengecekan kelengkapan dari TypeScript adalah fitur yang kuat yang memastikan Anda menangani semua state yang mungkin dalam state machine Anda. Jika Anda menambahkan state baru ke union `OrderState` tetapi lupa memperbarui fungsi `processOrder`, TypeScript akan menandai sebuah galat.
Untuk mengaktifkan pengecekan kelengkapan, Anda dapat menggunakan tipe `never`. Di dalam kasus `default` dari pernyataan switch Anda, tetapkan state ke variabel dengan tipe `never`.
function processOrder(state: OrderState, action: Action): OrderState {
switch (state.type) {
// ... (kasus sebelumnya) ...
default:
const _exhaustiveCheck: never = state;
return _exhaustiveCheck; // Atau lemparkan galat
}
}
Jika pernyataan `switch` menangani semua nilai `OrderState` yang mungkin, variabel `_exhaustiveCheck` akan bertipe `never` dan kode akan berhasil dikompilasi. Namun, jika Anda menambahkan state baru ke union `OrderState` dan lupa menanganinya di pernyataan `switch`, variabel `_exhaustiveCheck` akan memiliki tipe yang berbeda, dan TypeScript akan melemparkan galat waktu kompilasi (compile-time error), memberitahu Anda tentang kasus yang hilang.
Contoh Praktis dan Aplikasi
Discriminated unions dapat diterapkan dalam berbagai skenario selain sistem pemrosesan pesanan sederhana:
- Manajemen State UI: Memodelkan state dari komponen UI (mis., loading, success, error).
- Penanganan Permintaan Jaringan: Merepresentasikan berbagai tahapan permintaan jaringan (mis., initial, in progress, success, failure).
- Validasi Formulir: Melacak validitas field formulir dan state formulir secara keseluruhan.
- Pengembangan Game: Mendefinisikan berbagai state dari karakter atau objek game.
- Alur Otentikasi: Mengelola state otentikasi pengguna (mis., logged in, logged out, pending verification).
Contoh: Manajemen State UI
Mari kita pertimbangkan contoh sederhana dalam mengelola state komponen UI yang mengambil data dari API. Kita dapat mendefinisikan state berikut:
interface Initial {
type: "initial";
}
interface Loading {
type: "loading";
}
interface Success {
type: "success";
data: T;
}
interface Error {
type: "error";
message: string;
}
type UIState = Initial | Loading | Success | Error;
function renderUI(state: UIState): React.ReactNode {
switch (state.type) {
case "initial":
return Klik tombol untuk memuat data.
;
case "loading":
return Memuat...
;
case "success":
return {JSON.stringify(state.data, null, 2)}
;
case "error":
return Galat: {state.message}
;
default:
const _exhaustiveCheck: never = state;
return _exhaustiveCheck;
}
}
Contoh ini menunjukkan bagaimana discriminated unions dapat digunakan untuk mengelola berbagai state dari komponen UI secara efektif, memastikan UI dirender dengan benar berdasarkan state saat ini. Fungsi `renderUI` menangani setiap state dengan tepat, menyediakan cara yang jelas dan type-safe untuk mengelola UI.
Praktik Terbaik Menggunakan Discriminated Unions
Untuk memanfaatkan discriminated unions secara efektif dalam proyek TypeScript Anda, pertimbangkan praktik terbaik berikut:
- Pilih Nama Diskriminan yang Bermakna: Pilih nama diskriminan yang dengan jelas menunjukkan tujuan properti tersebut (mis., `type`, `state`, `status`).
- Jaga Agar Data State Tetap Minimal: Setiap state seharusnya hanya berisi data yang relevan dengan state spesifik tersebut. Hindari menyimpan data yang tidak perlu dalam state.
- Gunakan Pengecekan Kelengkapan: Selalu aktifkan pengecekan kelengkapan untuk memastikan Anda menangani semua state yang mungkin.
- Pertimbangkan Menggunakan Pustaka Manajemen State: Untuk state machine yang kompleks, pertimbangkan menggunakan pustaka manajemen state khusus seperti XState, yang menyediakan fitur-fitur canggih seperti state charts, state hierarkis, dan state paralel. Namun, untuk skenario yang lebih sederhana, discriminated unions mungkin sudah cukup.
- Dokumentasikan State Machine Anda: Dokumentasikan dengan jelas berbagai state, transisi, dan action dari state machine Anda untuk meningkatkan kemudahan pemeliharaan dan kolaborasi.
Teknik Tingkat Lanjut
Conditional Types
Conditional types dapat digabungkan dengan discriminated unions untuk menciptakan state machine yang lebih kuat dan fleksibel. Sebagai contoh, Anda dapat menggunakan conditional types untuk mendefinisikan tipe kembalian yang berbeda untuk sebuah fungsi berdasarkan state saat ini.
function getData(state: UIState): T | undefined {
if (state.type === "success") {
return state.data;
}
return undefined;
}
Fungsi ini menggunakan pernyataan `if` sederhana tetapi bisa dibuat lebih kokoh menggunakan conditional types untuk memastikan tipe tertentu selalu dikembalikan.
Utility Types
Utility types dari TypeScript, seperti `Extract` dan `Omit`, dapat sangat membantu saat bekerja dengan discriminated unions. `Extract` memungkinkan Anda untuk mengekstrak anggota spesifik dari sebuah tipe union berdasarkan kondisi, sementara `Omit` memungkinkan Anda untuk menghapus properti dari sebuah tipe.
// Ekstrak state "success" dari union UIState
type SuccessState = Extract, { type: "success" }>;
// Hilangkan properti 'message' dari interface Error
type ErrorWithoutMessage = Omit;
Contoh Dunia Nyata di Berbagai Industri
Kekuatan discriminated unions meluas ke berbagai industri dan domain aplikasi:
- E-commerce (Global): Dalam platform e-commerce global, status pesanan dapat direpresentasikan dengan discriminated unions, menangani state seperti "PaymentPending", "Processing", "Shipped", "InTransit", "Delivered", dan "Cancelled". Ini memastikan pelacakan dan komunikasi yang benar di berbagai negara dengan logistik pengiriman yang bervariasi.
- Jasa Keuangan (Perbankan Internasional): Mengelola state transaksi seperti "PendingAuthorization", "Authorized", "Processing", "Completed", "Failed" sangatlah penting. Discriminated unions menyediakan cara yang kokoh untuk menangani state ini, mematuhi berbagai regulasi perbankan internasional.
- Kesehatan (Pemantauan Pasien Jarak Jauh): Merepresentasikan status kesehatan pasien menggunakan state seperti "Normal", "Warning", "Critical" memungkinkan intervensi tepat waktu. Dalam sistem layanan kesehatan yang terdistribusi secara global, discriminated unions dapat memastikan interpretasi data yang konsisten terlepas dari lokasi.
- Logistik (Rantai Pasokan Global): Melacak status pengiriman melintasi batas internasional melibatkan alur kerja yang kompleks. State seperti "CustomsClearance", "InTransit", "AtDistributionCenter", "Delivered" sangat cocok untuk implementasi discriminated union.
- Pendidikan (Platform Pembelajaran Online): Mengelola status pendaftaran kursus dengan state seperti "Enrolled", "InProgress", "Completed", "Dropped" dapat memberikan pengalaman belajar yang lebih efisien, yang dapat disesuaikan dengan berbagai sistem pendidikan di seluruh dunia.
Kesimpulan
Discriminated unions di TypeScript menyediakan cara yang kuat dan type-safe untuk membangun state machine. Dengan mendefinisikan state dan transisi yang mungkin secara jelas, Anda dapat membuat kode yang lebih kokoh, mudah dipelihara, dan dapat dimengerti. Kombinasi dari keamanan tipe, pengecekan kelengkapan, dan pelengkapan kode yang ditingkatkan membuat discriminated unions menjadi alat yang tak ternilai bagi setiap pengembang TypeScript yang berurusan dengan manajemen state yang kompleks. Terapkan discriminated unions di proyek Anda berikutnya dan rasakan manfaat manajemen state yang type-safe secara langsung. Seperti yang telah kami tunjukkan dengan berbagai contoh dari e-commerce hingga kesehatan, dan logistik hingga pendidikan, prinsip manajemen state yang type-safe melalui discriminated unions dapat diterapkan secara universal.
Baik Anda membangun komponen UI sederhana atau aplikasi perusahaan yang kompleks, discriminated unions dapat membantu Anda mengelola state secara lebih efektif dan mengurangi risiko galat saat runtime. Jadi, selami dan jelajahi dunia state machine yang type-safe dengan TypeScript!